--[[
Simple Fast Inventory (SFInv) is a mod found in Minetest Game that is used to
create the player's inventory formspec. SFInv comes with an API that allows you
to add and otherwise manage the pages shown.

Whilst SFInv by default shows pages as tabs, pages are called pages because it
is entirely possible that a mod or game decides to show them in some other
format instead. For example, multiple pages could be shown in one formspec.
]]
---@class SFInv
---@field pages table Table of `pages[pagename] = def`.
---@field pages_unordered table[] Array table of pages in order of addition (used to build navigation tabs).
---@field contexts table Table of `contexts[playername] = player_context`.
---@field enabled boolean Set to false to disable. Good for inventory rehaul mods like unified inventory.
sfinv = {
  pages = {},
  pages_unordered = {},
  contexts = {},
  enabled = true,
}

---@alias SFInvFields string[] A list of page names.

---@class SFInvContext
---@field page string Current page name.
---@field nav SFInvFields List of page names.
---@field nav_titles string[] List of page titles.
---@field nav_idx integer Current nav index (in nav and nav_titles).

-- SFInv page definition.
---@class SFInvDef
---@field name string|nil Technical page name.
---@field title string Human readable page name.
---@field get fun(self:SFInv, player:mt.PlayerObjectRef, context:SFInvContext): string Returns a formspec string.
---@field is_in_nav nil|fun(self:SFInv, player:mt.PlayerObjectRef, context:SFInvContext): boolean Returns true to show in the navigation (the tab header, by default).
---@field on_player_receive_fields nil|fun(self:SFInv, player:mt.PlayerObjectRef, context:SFInvContext, fields: SFInvFields) On formspec submit.
---@field on_enter nil|fun(self:SFInv, player:mt.PlayerObjectRef, contextcontext:SFInvContext) Called when the player changes pages, usually using the tabs.
---@field on_leave nil|fun(self:SFInv, player:mt.PlayerObjectRef, context:SFInvContext) When leaving this page to go to another, called before other's on_enter.

-- Register a page
---@param name string
---@param def SFInvDef
function sfinv.register_page(name, def)
  assert(name, "Invalid sfinv page. Requires a name")
  assert(def, "Invalid sfinv page. Requires a def[inition] table")
  assert(def.get, "Invalid sfinv page. Def requires a get function.")
  assert(
    not sfinv.pages[name],
    "Attempt to register already registered sfinv page " .. dump(name)
  )

  sfinv.pages[name] = def
  def.name = name
  table.insert(sfinv.pages_unordered, def)
end

-- Overrides fields of an page registered with register_page.
---@param name string
---@param def SFInvDef
function sfinv.override_page(name, def)
  assert(name, "Invalid sfinv page override. Requires a name")
  assert(def, "Invalid sfinv page override. Requires a def[inition] table")
  local page = sfinv.pages[name]
  assert(
    page,
    "Attempt to override sfinv page " .. dump(name) .. " which does not exist."
  )
  for key, value in pairs(def) do
    page[key] = value
  end
end

-- Creates tabheader or "".
---@param player mt.PlayerObjectRef
---@param context SFInvContext
---@param nav SFInvFields A list of page names.
---@param current_idx integer Current nav index (in nav and nav_titles).
---@return string formspec
---@diagnostic disable-next-line: duplicate-set-field
function sfinv.get_nav_fs(player, context, nav, current_idx)
  -- Only show tabs if there is more than one page.
  if #nav > 1 then
    return "tabheader[0,0;sfinv_nav_tabs;"
      .. table.concat(nav, ",")
      .. ";"
      .. current_idx
      .. ";true;false]"
  else
    return ""
  end
end

local theme_inv = [[
  list[current_player;main;0,5.2;8,1;]
  list[current_player;main;0,6.35;8,3;8]
]]

-- Adds a theme to a formspec.
---@param player mt.PlayerObjectRef
---@param context SFInvContext
---@param content string Formspec.
---@param show_inv boolean|nil
---@param size string|nil
---@return string formspec
---@diagnostic disable-next-line: duplicate-set-field
function sfinv.make_formspec(player, context, content, show_inv, size)
  local tmp = {
    size or "size[8,9.1]",
    sfinv.get_nav_fs(player, context, context.nav_titles, context.nav_idx),
    show_inv and theme_inv or "",
    content,
  }
  return table.concat(tmp, "")
end

-- Get the page name of the first page to show to a player.
---@param player mt.PlayerObjectRef
---@return string
function sfinv.get_homepage_name(player) return "sfinv:crafting" end

-- Builds current page's formspec.
---@param player mt.PlayerObjectRef
---@param context SFInvContext
---@return string
function sfinv.get_formspec(player, context)
  -- Generate navigation tabs
  local nav = {}
  local nav_ids = {}
  local current_idx = 1
  for i, pdef in pairs(sfinv.pages_unordered) do
    if not pdef.is_in_nav or pdef:is_in_nav(player, context) then
      nav[#nav + 1] = pdef.title
      nav_ids[#nav_ids + 1] = pdef.name
      if pdef.name == context.page then current_idx = #nav_ids end
    end
  end
  context.nav = nav_ids
  context.nav_titles = nav
  context.nav_idx = current_idx

  -- Generate formspec
  local page = sfinv.pages[context.page] or sfinv.pages["404"]
  if page then
    return page:get(player, context)
  else
    local old_page = context.page
    local home_page = sfinv.get_homepage_name(player)

    if old_page == home_page then
      minetest.log(
        "error",
        "[sfinv] Couldn't find "
          .. dump(old_page)
          .. ", which is also the old page"
      )

      return ""
    end

    context.page = home_page
    assert(sfinv.pages[context.page], "[sfinv] Invalid homepage")
    minetest.log(
      "warning",
      "[sfinv] Couldn't find " .. dump(old_page) .. " so switching to homepage"
    )

    return sfinv.get_formspec(player, context)
  end
end

-- Gets the player's context.
---@param player mt.PlayerObjectRef
---@return SFInvContext
function sfinv.get_or_create_context(player)
  local name = player:get_player_name()
  local context = sfinv.contexts[name]
  if not context then
    context = {
      page = sfinv.get_homepage_name(player),
    }
    sfinv.contexts[name] = context
  end
  return context
end

---@param player mt.PlayerObjectRef
---@param context SFInvContext
function sfinv.set_context(player, context)
  sfinv.contexts[player:get_player_name()] = context
end

-- (Re)builds page formspec and calls `set_inventory_formspec()`.
---@param player mt.PlayerObjectRef
---@param context SFInvContext|nil
function sfinv.set_player_inventory_formspec(player, context)
  local fs =
    sfinv.get_formspec(player, context or sfinv.get_or_create_context(player))
  player:set_inventory_formspec(fs)
end

-- Changes the page.
---@param player mt.PlayerObjectRef
---@param pagename string
function sfinv.set_page(player, pagename)
  local context = sfinv.get_or_create_context(player)
  local oldpage = sfinv.pages[context.page]
  if oldpage and oldpage.on_leave then oldpage:on_leave(player, context) end
  context.page = pagename
  local page = sfinv.pages[pagename]
  if page.on_enter then page:on_enter(player, context) end
  sfinv.set_player_inventory_formspec(player, context)
end

---@param player mt.PlayerObjectRef
---@return string
function sfinv.get_page(player)
  local context = sfinv.contexts[player:get_player_name()] ---@type SFInvContext
  return context and context.page or sfinv.get_homepage_name(player)
end

minetest.register_on_joinplayer(function(player)
  if sfinv.enabled then sfinv.set_player_inventory_formspec(player) end
end)

minetest.register_on_leaveplayer(
  function(player) sfinv.contexts[player:get_player_name()] = nil end
)

minetest.register_on_player_receive_fields(function(player, formname, fields)
  if formname ~= "" or not sfinv.enabled then return false end

  -- Get Context
  local name = player:get_player_name()
  local context = sfinv.contexts[name]
  if not context then
    sfinv.set_player_inventory_formspec(player)
    return false
  end

  -- Was a tab selected?
  if fields.sfinv_nav_tabs and context.nav then
    local tid = tonumber(fields.sfinv_nav_tabs)
    if tid and tid > 0 then
      local id = context.nav[tid]
      local page = sfinv.pages[id]
      if id and page then sfinv.set_page(player, id) end
    end
  else
    -- Pass event to page
    local page = sfinv.pages[context.page]
    if page and page.on_player_receive_fields then
      return page:on_player_receive_fields(player, context, fields)
    end
  end
end)
